/****************************************************************
** 
** Copyright (C) 2007 Sean Collins sean@robothusbandry.com
** 
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License
** as published by the Free Software Foundation; either version 2
** of the License, or (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
** 
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
** MA  02110-1301, USA.
**
** The GNU General Public License is in the file GPL.txt
**
****************************************************************/
package gds2062;

import java.awt.Graphics;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseListener;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;

import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.HashMap;

import java.util.prefs.Preferences;

import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JPanel;
import javax.swing.UIManager;

import java.text.NumberFormat;

import gnu.io.CommPort;
import gnu.io.RXTXPort;
import gnu.io.SerialPort;
import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.UnsupportedCommOperationException;
import gnu.io.RXTXCommDriver;

public class GDS2062 extends JFrame
{
  /* for Serializable interface */
  private static final long serialVersionUID = 0;
  private Preferences prefGDS2062;
	private GDS2062 jfThis;
  private ScopeDisplay jpScope;
  private static RXTXPort rtScope;
  private static byte[] bigByteBuffer;

  private final static int SERIAL_REPLY_SLEEP = 40;
  private final static int SERIAL_REPLY_WAITS = 3;
  private static int iCommRetries = SERIAL_REPLY_WAITS;

  private final static int TOP_TEXT_Y = 12;
  private final static int TOP_LABEL_Y = 2;
  private final static int SEAN_LABEL_X = 2;

  private final static int STOP_X = 400;

  /* bottom content has y UP FROM BOTTOM OF WINDOW! */
  private final static int[] CHANNEL_VOLTS_X = {2, 100};
  private final static int SCREEN_BOTTOM_FIRST_ROW = 32;
  private final static int SCREEN_BOTTOM_SECOND_ROW = 3;
  private final static int TIMEDIV_X = 100;
  private final static int TIMEDIV_Y = SCREEN_BOTTOM_SECOND_ROW;
  private final static int TRIGGER_STATUS_X = 300;
  private final static int TRIGGER_STATUS_Y = SCREEN_BOTTOM_FIRST_ROW;

  private final static int LEFT_X = 2;

	private final static HashMap<String, String> mapTimeScaleStrings = new HashMap<String, String>();
	private static NumberFormat format3places = NumberFormat.getInstance();
	static
	{
		mapTimeScaleStrings.put("1.000E-09", "1ns");
		mapTimeScaleStrings.put("2.500E-09", "2.5ns");
		mapTimeScaleStrings.put("5.000E-09", "5ns");
		mapTimeScaleStrings.put("1.000E-08", "10ns");
		mapTimeScaleStrings.put("2.500E-08", "25ns");
		mapTimeScaleStrings.put("5.000E-08", "50ns");
		mapTimeScaleStrings.put("1.000E-07", "100ns");
		mapTimeScaleStrings.put("2.500E-07", "250ns");
		mapTimeScaleStrings.put("5.000E-07", "500ns");
		mapTimeScaleStrings.put("1.000E-06", "1us");
		mapTimeScaleStrings.put("2.500E-06", "2.5us");
		mapTimeScaleStrings.put("5.000E-06", "5us");
		mapTimeScaleStrings.put("1.000E-05", "10us");
		mapTimeScaleStrings.put("2.500E-05", "25us");
		mapTimeScaleStrings.put("5.000E-05", "50us");
		mapTimeScaleStrings.put("1.000E-04", "100us");
		mapTimeScaleStrings.put("2.500E-04", "250us");
		mapTimeScaleStrings.put("5.000E-04", "500us");
		mapTimeScaleStrings.put("1.000E-03", "1ms");
		mapTimeScaleStrings.put("2.500E-03", "2.5ms");
		mapTimeScaleStrings.put("5.000E-03", "5ms");
		mapTimeScaleStrings.put("1.000E-02", "10ms");
		mapTimeScaleStrings.put("2.500E-02", "25ms");
		mapTimeScaleStrings.put("5.000E-02", "50ms");
		mapTimeScaleStrings.put("1.000E-01", "100ms");
		mapTimeScaleStrings.put("2.500E-01", "250ms");
		mapTimeScaleStrings.put("5.000E-01", "500ms");
		mapTimeScaleStrings.put("1.000E-00", "1s");
		mapTimeScaleStrings.put("2.500E-00", "2.5s");
		mapTimeScaleStrings.put("5.000E-00", "5s");
		mapTimeScaleStrings.put("1.000E+01", "10s");
		
		format3places.setMaximumFractionDigits(3);
		format3places.setMinimumFractionDigits(3);
	}

  public static void main(String args[]) throws Exception
  {
    try {
        UIManager.setLookAndFeel(
            UIManager.getCrossPlatformLookAndFeelClassName());
    } catch (Exception e) { }

    new GDS2062();
  }

  public GDS2062()
  {
    super("GDS2062");
    jfThis = this;
    setDefaultCloseOperation(DISPOSE_ON_CLOSE);
		prefGDS2062 = Preferences.userNodeForPackage(this.getClass());
    bigByteBuffer = new byte[200000];
// connect to scope
    rtScope = null;
    HashSet<SerialPortParams> h = new HashSet<SerialPortParams>();
// This RXTX port discovery just isn't floating my boat. Use a manual method to
// harvest port names from the user.
		String sPorts = prefGDS2062.get("Ports", "");
		if (sPorts.equals(""))
			sPorts = "/dev/ttyS0,9600,8,n,1:/dev/ttyACM0,38400,8,n,1";
		String sUserPorts = JOptionPane.showInputDialog(this, "Colon-separated list of ports to scan for GDS2062\nSpecify as port,baud,parity,databits,stopbits (like /dev/ttyS0,9600,n,8,1)", sPorts);
		if (sUserPorts != null)
		{
			prefGDS2062.put("Ports", sUserPorts);
			sPorts = sUserPorts;
		}
		for(String sPort : sPorts.split(":"))
    {
          try
          {
						SerialPortParams spp = SerialPortParams.fromString(sPort);
						RXTXPort thePort = spp.getRXTXPort();
            thePort.close();
            h.add(spp);
            System.err.println(sPort + " exists...");
          }
          catch (PortInUseException e)
          {
            System.err.println("In use: " + sPort);
          }
          catch (Exception e)
          {
            System.err.println("Failed to open port " + sPort + ": " + e);
            // e.printStackTrace();
          }
    } // while there are registered serial ports
    // that's the hashset of potential ports built
    
    // now check the serial ports found
    for (SerialPortParams spp : h)
    {
			try
			{
				RXTXPort thePort = spp.getRXTXPort();
				initScope(thePort);
				if (rtScope != null)
					break;
				thePort.close();
			}
			catch (Exception e)
			{
				e.printStackTrace();
			}
    } // for all detected serial ports
    if (rtScope == null)
    {
			System.err.println("No scope found.");
      System.exit(1);
    }
// end of connect to scope
    jpScope = new ScopeDisplay();
    jfThis.setContentPane(jpScope);
    this.pack();
    jfThis.setSize(600, 600);
    jfThis.setVisible(true);
    jpScope.refresh();
  }
	public void dispose()
	{
		super.dispose();
		rtScope.close();
		System.err.println("disposed");
	}
  public static byte[] commandReply(String command)
  {
    byte[] b = null;
    int ib = 0;
    InputStream in;
    OutputStream out;
		in = rtScope.getInputStream();
		out = rtScope.getOutputStream();
		try
		{
			int len = 0;
			int lastLen = 0;
			b = bigByteBuffer;
			// is the input stream empty? It should be!
			while (in.available() != 0)
			{
				len = in.read(b, 0, in.available());
				System.err.print("X-(");
				System.err.println("Found " + len + " bytes");
				Thread.sleep(SERIAL_REPLY_SLEEP);
			}
			len = 0;
			System.err.println("commandReply sent " + command);
			out.write(command.getBytes());
			out.write(0x0a);
			if (command.endsWith("?")) // expecting a reply
			{
				b = bigByteBuffer;
				for (int i = 0; i < iCommRetries; i++)
				{
					len = in.available();
					if (len > 0)
					{
						len = in.read(b, ib, len);
						ib += len;
						i = 0; // wait some more!
						System.err.print("+");
						System.err.println(len);
					}
					else // no bytes rx
						System.err.print("_"); // keep waiting
					Thread.sleep(SERIAL_REPLY_SLEEP);
				} // for as long as we want to wait for a reply
				if (ib > 0)
					b = Arrays.copyOf(b, ib);
				else
					b = null;
			} // if command endsWith ?
			else
			{
				b = null;
				Thread.sleep(100); // needs a delay between successive commands?
			}
		} // try sending the command
		catch (Exception e)
		{
			e.printStackTrace();
		}
    if (b != null)
    {
      System.err.println("commandReply returns " + byteString(b) + " as String:" + new String(b));
    }
    return b;
  } // byte[] commandReply(String command)

  private void initScope(RXTXPort rxtxX)
  {
    // is it our scope?
    try
    {
      String sIn = null;
      rtScope = rxtxX;
      byte[] b = commandReply("*IDN?");
      rtScope = null;
      if (b != null)
        sIn = new String(b);
      if (sIn == null)
        System.err.println("\nNo reply from " + rxtxX.getName());
      else if (sIn.indexOf("GW,") == 0)
      {
        String[] sSplit = sIn.split(",");
        if (sSplit.length != 4)
          System.err.println("Identification string garbled: " + sIn);
        else
          if (sSplit[1].equals("GDS-2062"))
          {
            System.err.println("GDS-2062 on " + rxtxX.getName());
            rtScope = rxtxX;
          }
          else
            System.err.println("Not GDS-2062: " + sIn);
      }
    }
    catch(Exception e)
    {
      e.printStackTrace();
      rtScope = null;
    } 
  } // void initScope(RXTXPort)
/********************************************************************
**
** This is ScopeDisplay - the panel that displays all the data
**
********************************************************************/
  public class ScopeDisplay extends JPanel
  {
    /* for Serializable interface */
    private static final long serialVersionUID = 0;
    private Waveform channel1;
    private Waveform channel2;

    private int tmpX;
    private int tmpY;

    private int width;
    private int height;
    private int iInset;
    private float xScale;
    private float yScale;
    private int yZero;
    private String sTimeOffset;
    private float fTimeOffset;
    private String sTimeDiv;
    private float fTimeDiv;
    private int iGraticuleType;
    private boolean bCircleSamples; // draw rings around the sample points
    private boolean bShowValues; // draw sample values next to points
    private boolean bSampleToolTips; // show point values on tooltip

    private StopLabel lblStop;
    private GDS2062Label lblGDS2062;
    private TimeDivLabel lblTimeDiv;
    private TimeOffsetLabel lblTimeOffset;
    private TriggerStatusLabel lblTriggerStatus;

    private SelectRect selectBox;
    private JPopupMenu jpScopeMenu;

    public final Color[] CHANNEL_COLOUR = {Color.RED, Color.YELLOW, Color.CYAN};

    public ScopeDisplay()
    {
      super();
      setOpaque(true);
      bCircleSamples = false;
      bShowValues = false;
      bSampleToolTips = false;
      channel1 = new Waveform(1);
      channel2 = new Waveform(2);
      this.add(channel1.lblOffset);
      this.add(channel2.lblOffset);
      this.add(lblGDS2062 = new GDS2062Label());
      this.add(lblStop = new StopLabel());
      this.add(lblTimeDiv = new TimeDivLabel());
      this.add(lblTimeOffset = new TimeOffsetLabel());
      this.add(lblTriggerStatus = new TriggerStatusLabel());
      jpScopeMenu = new ScopeDisplayMenu();
      selectBox = new SelectRect(0, 0);
      MouseAdapter ma = new ScopeDisplayMouseAdapter();
      addMouseListener(ma);
      addMouseMotionListener(ma);
    }
    public void paintComponent(Graphics g)
    {
      super.paintComponent(g);
      lblStop.setLocation(STOP_X, TOP_LABEL_Y);
      lblGDS2062.setLocation(2, TOP_LABEL_Y);
      lblTimeDiv.setLocation(TIMEDIV_X, height - SCREEN_BOTTOM_FIRST_ROW);
      lblTriggerStatus.setLocation(TRIGGER_STATUS_X, height - TRIGGER_STATUS_Y);
      width = getSize().width;
      height = getSize().height;
      iInset = 20;
      yScale = (height  - 3 * iInset) / 200f;
      yZero = (int)(yScale * 100) + iInset;
      g.setColor(Color.BLUE);
      g.fillRect(0, 0, width, height);
      g.setColor(Color.BLACK);
      g.fillRect(iInset, iInset, width - 2 * iInset, (int)(200 * yScale));
      g.setColor(Color.DARK_GRAY);
      if (!channel1.isAvailable && !channel2.isAvailable)
        return; // nothing to draw
      g.drawRect(iInset, iInset, width - 2 * iInset, (int)(200 * yScale));
      if (iGraticuleType < 2)
      {
        // vertical
        g.drawLine(width / 2 - 1, iInset, width / 2 - 1, iInset + (int)(200 * yScale));
        g.drawLine(width / 2, iInset, width / 2, iInset + (int)(200 * yScale));
        g.drawLine(width / 2 + 1, iInset, width / 2 + 1, iInset + (int)(200 * yScale));
        // horizontal
        g.drawLine(iInset, yZero - 1,  width  - iInset, yZero - 1);
        g.drawLine(iInset, yZero,  width  - iInset, yZero);
        g.drawLine(iInset, yZero + 1,  width  - iInset, yZero + 1);
      }
      if (iGraticuleType < 1)
      {
        float graticuleSpacing = (200 * yScale) / 8;
        float graticulePos = iInset + graticuleSpacing;
        for (int y = 0; y < 8; y++)
        {
          g.drawLine(iInset, (int)graticulePos, width - iInset, (int)graticulePos);
          graticulePos += graticuleSpacing;
        }
        float fSampleSpacing = 0;
        int iSampleCount = 0;
        if (channel1.isAvailable)
        {
          fSampleSpacing = channel1.fInterval;
          iSampleCount = channel1.aiPoints.length;
        }
        else if (channel2.isAvailable)
        {
          fSampleSpacing = channel2.fInterval;
          iSampleCount = channel2.aiPoints.length;
        }
        // System.err.println("fSampleSpacing is " + fSampleSpacing);
        float fSampleEpoch = iSampleCount * fSampleSpacing; // sample window duration
        // System.err.println("SampleEpoch is " + fSampleEpoch);
        int xMax = 20; // full memory from scope (int)(fSampleEpoch / fTimeDiv);
        ////  System.err.println("xMax is " + xMax);
        graticuleSpacing = (width - 2f * iInset) / xMax;
        // System.err.println("graticuleSpacing is " + graticuleSpacing);
        graticulePos = iInset + graticuleSpacing;
        // System.err.println("graticulePos is " + graticulePos);
        for (int x = 0; x < xMax; x++)
        {
          g.drawLine((int)graticulePos, iInset, (int)graticulePos, (int)(iInset + 200 * yScale));
          graticulePos += graticuleSpacing;
        }
      }
      // draw time offset arrow
      g.setColor(Color.YELLOW);
      int[] aiArrowX = new int[4];
      int[] aiArrowY = new int[4];
      final int ARROW_SIZE = 7;
      aiArrowX[0] = width / 2 - ARROW_SIZE; // base left
      aiArrowY[0] = iInset + (int)(200 * yScale) - 1;
      aiArrowX[1] = width / 2 + ARROW_SIZE; // base right
      aiArrowY[1] = iInset + (int)(200 * yScale) - 1;
      aiArrowX[2] = width / 2; // tip
      aiArrowY[2] = iInset + (int)(200 * yScale) - ARROW_SIZE - 1;
      aiArrowX[3] = width / 2 - ARROW_SIZE; // base left
      aiArrowY[3] = iInset + (int)(200 * yScale) - 1;
      g.fillPolygon(aiArrowX, aiArrowY, 4);
      lblTimeOffset.setLocation(aiArrowX[2] + ARROW_SIZE, aiArrowY[2] - lblTimeOffset.getHeight());
      paintChannel(channel1, g);
      paintChannel(channel2, g);
      if (selectBox.isVisible)
        selectBox.paintBox(g);
    } // ScopeDisplay.paintComponent
    private void paintChannel(Waveform w, Graphics g)
    {
      if (w.aiPoints != null) // acquired
      {
        g.setColor(w.colour);
        float yOffset = 25f * w.fOffset / w.fScale; // 25 = 200 / 8 divisions
        float lastX = iInset;
        float lastY = yZero - (w.aiPoints[0] + yOffset) * yScale;
        xScale = (width - 2f * iInset) / w.aiPoints.length;
        // Channel offset marker
        tmpX = LEFT_X;
        tmpY = (int)(yZero - yOffset * yScale);
        w.lblOffset.setLocation(tmpX, tmpY - w.lblOffset.getHeight() / 2);
        for (int x = 0; x < w.aiPoints.length; x++)
        {
          float thisY = yZero - (w.aiPoints[x] + yOffset) * yScale;
          float thisX = x * xScale + iInset;
          g.drawLine((int)lastX, (int)lastY, (int)thisX, (int)thisY);
          if (bCircleSamples)
            g.drawOval((int)thisX - 3, (int)thisY - 3, 6, 6);
          if (bShowValues)
            g.drawString(w.aiPoints[x] + " (" + w.pointValueString(x) + ")", (int)thisX + 4, (int)thisY + 2);
          lastY = thisY; 
          lastX = thisX;
        }
        g.drawString("CH" + w.iChannel + ":" + w.sScale, CHANNEL_VOLTS_X[w.iChannel - 1], height - SCREEN_BOTTOM_SECOND_ROW);
      }
      else // not yet acquired
        g.drawString("Channel " + w.iChannel + " not acquired", iInset + 5, iInset + w.iChannel * 20);
    } // paintChannel
    public void refresh()
    {
      jpScope.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
      selectBox.isVisible = false;
      byte[] b = null;
      b = commandReply(":TIMEBASE:SCALE?");
      sTimeDiv = bytes2StringNoLF(b);
      fTimeDiv = Float.parseFloat(sTimeDiv);
      lblTimeDiv.setText(mapTimeScaleStrings.get(sTimeDiv));
      lblTimeOffset.refresh();
      lblTriggerStatus.refresh();
      b = commandReply(":DISPLAY:GRATICULE?");
      String sReply = bytes2StringNoLF(b);
      iGraticuleType = Integer.parseInt(sReply);
      lblStop.setStopped(true); // probably...
      channel1.refresh();
      channel2.refresh();
      jpScope.repaint();
      jpScope.setCursor(Cursor.getDefaultCursor());
    } // refresh
    private void setMousePos(int x, int y)
    {
      String tooltip = null;
      if (!bSampleToolTips)
        return;
      if (x < iInset)
        return;
      int sampleValueY;
      float yOffset;
      int sampleIndex = (int)((x - iInset) / xScale);
      int sampleIndexMax = sampleIndex;
      if (sampleIndex < 0)
      {
        this.setToolTipText(null);
        return;
      }
      int sampleValue;
      if (channel1.isAvailable)
      {
        if (sampleIndex < channel1.aiPoints.length)
        {
          sampleIndexMax = sampleIndex + (int)(2f / xScale);
          if (sampleIndexMax > channel1.aiPoints.length)
            sampleIndexMax = channel1.aiPoints.length;
          sampleIndex -= (int)(1f / xScale);
        }
        else // pointer to the right of samples
        {
          this.setToolTipText(null);
          return; // both sample sets the same length
        }

        yOffset = 25f * channel1.fOffset / channel1.fScale; // y offset unscaled

        for (int ix = sampleIndex; ix < sampleIndexMax; ix++)
        {
          sampleValue = channel1.aiPoints[ix];
          sampleValueY = (int)(yZero - (sampleValue + yOffset) * yScale);
          if (Math.abs(sampleValueY - y) < 3)
          {
            tooltip = "CH1: " + channel1.pointValueString(ix);
            break;
          }
        }
      } // if channel1.isAvailable
      // try channel2
      if (channel2.isAvailable)
      {
        if (sampleIndex < channel2.aiPoints.length)
        {
          sampleIndexMax = sampleIndex + (int)(2f / xScale);
          if (sampleIndexMax > channel2.aiPoints.length)
            sampleIndexMax = channel2.aiPoints.length;
          sampleIndex -= (int)(1f / xScale);
        }
        else // pointer to the right of samples
          return; // nothing to do
        yOffset = 25f * channel2.fOffset / channel2.fScale;
        for (int ix = sampleIndex; ix < sampleIndexMax; ix++)
        {
          sampleValue = channel2.aiPoints[ix];
          sampleValueY = (int)(yZero - (sampleValue + yOffset) * yScale);
          if (Math.abs(sampleValueY - y) < 3) // close enough
          {
            if (tooltip != null)
              tooltip += " CH2:" + channel2.pointValueString(ix);
            else
              tooltip = "CH2:" + channel2.pointValueString(ix);
            break;
          }
        } // for all possible indexes
      } // if channel2 is available
      this.setToolTipText(tooltip);
    } // setMousePos
    class Waveform
    {
      public int[] aiPoints;
      private float fInterval;
      public int iChannel;
      public String sScale;
      public String sScaleUnit;
      public float fScale;
      public String sOffset; // offset volts
      public float fOffset; // offset volts
      public boolean isAvailable;

      public Color colour;

      public OffsetLabel lblOffset;
      
      public int memoryLength = 0; // short memory

      public Waveform(int channel)
      {
        aiPoints = null;
        iChannel = channel;
        colour = CHANNEL_COLOUR[iChannel];
        lblOffset = new OffsetLabel();
        isAvailable = false;
      }
      public float pointValue(int x)
      /* int x is sample value, returns volts value*/
      {
        return x / 25f * fScale;
      }
      public String pointValueString2(float value)
      /* returns tidy string from float value, with volt units */
      {
        if (sScale.endsWith("mV"))
          return value * 1000f + "mv";
        else if (sScale.endsWith("V"))
          return value + "V";
        else
          return value + "?";
      }
      public String pointValueString(int index)
      /* index is for array of samples from scope */
      {
        return pointValueString2(pointValue(aiPoints[index]));
      }
      public void setData(byte[] dsoData)
      /* dsoData is byte buffer containing reply to "ACQUIREn:MEMORY?" */
      {
        if (dsoData == null)
          return;
        int i = 0;
        i = 0;
        if (dsoData[i] != '#')
        {
          System.err.println("Waveform missing leading '#'");
          // System.err.println(byteString(dsoData));
          return;
        }
        i++;
        int iLenLen = Character.digit(dsoData[i], 0x0a);
        i++;
        String sNum = new String(dsoData, i, iLenLen);
        int iNum = Integer.parseInt(sNum);
        aiPoints = new int[(iNum - 8) / 2];
        switch (aiPoints.length)
        {
          case 500:
          case 5000:
          case 12500:
          case 25000:
            if (dsoData.length < 2 * aiPoints.length + 14)
            {
              System.err.println("Insufficient data: scope says "
               + aiPoints.length
               + ", but received only "
               + dsoData.length
               + " bytes (should be scope * 2 + 14)");
              // System.err.println(byteString(dsoData));
              return;
            }
            break; // okay!
          default:
            if (dsoData.length < 2 * aiPoints.length + 14)
            {
              System.err.println("Insufficient data: scope says "
               + aiPoints.length
               + ", but received only "
               + dsoData.length
               + " bytes (should be scope * 2 + 14)");
              // System.err.println(byteString(dsoData));
              return;
            }
            else
              System.err.println("Strange waveform length: " + aiPoints.length + ", attempting to continue");
        } // switch sample set length
        i += iLenLen;
        // interval
        int iFloatBits = 0;
        iFloatBits |= byte2UnsignedInt(dsoData[i]);  // MSByte
        int iInterval = iFloatBits << 24;
        // System.err.println("iInterval:" + Integer.toString(iInterval, 0x10));
        i++;
        iFloatBits = byte2UnsignedInt(dsoData[i]);  // 2rd byte
        // System.err.println("iFloatBits:" + Integer.toString(iFloatBits, 0x10));
        iInterval |= (iFloatBits << 16);
        // System.err.println("iInterval:" + Integer.toString(iInterval, 0x10));
        i++;
        iFloatBits = byte2UnsignedInt(dsoData[i]);  // 3rd byte
        // System.err.println("iFloatBits:" + Integer.toString(iFloatBits, 0x10));
        iInterval |= (iFloatBits << 8);
        // System.err.println("iInterval:" + Integer.toString(iInterval, 0x10));
        i++;
        iFloatBits = byte2UnsignedInt(dsoData[i]);  // 4th byte
        // System.err.println("iFloatBits:" + Integer.toString(iFloatBits, 0x10));
        iInterval |= (iFloatBits);
        // System.err.println("iInterval:" + Integer.toString(iInterval, 0x10));
        fInterval = Float.intBitsToFloat(iInterval);
        i++;
        // channel ID
        iChannel = dsoData[i];
        i++;
        i += 3; //3 reserved bytes
        System.err.println("ready to parse " + aiPoints.length + " points from byte buffer[" + dsoData.length + "]");
        for (int p = 0; p < aiPoints.length; p++)
        {
          int iPoint = byte2UnsignedInt(dsoData[i++]);
          iPoint <<= 8;
          iPoint |= byte2UnsignedInt(dsoData[i++]);
          if (iPoint > 0x7fff) // -ve
            iPoint -= 0x10000;
          aiPoints[p] = iPoint;
        }
        isAvailable = true;
      } //setData
      public String toString()
      {
        String s = "Waveform channel " + iChannel + ", " + fInterval + "s interval\n";
        for (int p : aiPoints)
          s += p + ",";
        return s;
      }
      public void setOffset(String s)
      // scale string is reported by scope
      {
        sOffset = s;
        fOffset = GDS2062.parseVolt(s);
        lblOffset.setVisible(true);
      }
      public void setScale(String s)
      // scale string is reported by scope
      {
        sScale = s;
        fScale = GDS2062.parseVolt(s);
      }
      public void refresh()
      {
        byte[] b = null;
        String sReply = null;
        b = commandReply(":ACQUIRE:LENGTH " + memoryLength); // because the manual said so?
        b = commandReply(":ACQUIRE:LENGTH?"); // because the manual said so?
        // ACQUIRE:LENGTH seems not to reply sometimes...
        // System.err.println("Set acquire length replied " + new String(b));
        b = commandReply(":ACQUIRE" + iChannel + ":MEMORY?");
        setData(b);
        b = commandReply(":CHAN" + iChannel + ":SCALE?");
        sReply = bytes2StringNoLF(b);
				try
				{
					setScale(prettyNumber(sReply) + "V");
				}
				catch(Exception e)
				{ e.printStackTrace(); }
        b = commandReply(":CHAN" + iChannel + ":OFFSET?");
        sReply = bytes2StringNoLF(b);
				try{ setOffset(prettyNumber(sReply) + "V"); }
				catch(Exception e) { e.printStackTrace(); }
      } // refresh
      class OffsetLabel extends JLabel
      {
        /* for Serializable interface */
        private static final long serialVersionUID = 0;
        public OffsetLabel()
        {
          super();
					this.addMouseListener(new OffsetMouseAdapter());
					this.addMouseMotionListener(new OffsetMouseAdapter());
          setText(Integer.toString(iChannel) + ">");
          setForeground(colour);
          setVisible(false); //initially invisible
          setComponentPopupMenu(new OffsetMenu());
        }
				private void dragY(int change)
				{
				//TODO update offset on scope on drag of label
        /*float yOffset = 25f * w.fOffset / w.fScale; // 25 = 200 / 8 divisions
        tmpY = (int)(yZero - yOffset * yScale);
        w.lblOffset.setLocation(tmpX, tmpY - w.lblOffset.getHeight() / 2);*/
					System.err.println(change);
					Point p = this.getLocation();
					p.y += change;
					this.setLocation(p);
				}
				public void setVoltOffset(String s)
				{
					commandReply(":CHAN" + iChannel + ":OFFSET " + volt2Float(s));
					if (iChannel == 1)
						channel1.refresh();
					else
						channel2.refresh();
					jpScope.repaint();
				}
				class OffsetMouseAdapter extends MouseAdapter
				{
					private int startY = 0;
					public void mouseReleased(MouseEvent e)
					{
					}
					public void mousePressed(MouseEvent e)
					{
						startY = e.getY();
					}
					public void mouseDragged(MouseEvent e)
					{
						dragY(e.getY() - startY);
					}
				}
        class OffsetMenu extends JPopupMenu
        {
          /* for Serializable interface */
          private static final long serialVersionUID = 0;
          public OffsetMenu()
          {
            super();
            JMenu submenu;
            JMenuItem item;
						item = new JMenuItem("Offset Volts...");
						item.addActionListener(new ActionListener()
						 {
						  public void actionPerformed(ActionEvent e)
							{
								//TODO - no saved offset value, move offset handling to OffsetLabel
								// class and use 'refresh' style
								String s = JOptionPane.showInputDialog(jpScope, "Enter new volt offset for channel " + iChannel, sTimeOffset);
								if (s != null)
									setVoltOffset(s);
							}
						 }); // addActionListener
						add(item);
            submenu = new JMenu("Memory Length");
            submenu.setMnemonic(KeyEvent.VK_M);
            add(submenu);
            item = new JMenuItem("500");
            submenu.add(item);
            item.addActionListener(new ActionListener()
             {
              public void actionPerformed(ActionEvent e)
              {
                memoryLength = 0;
                commandReply(":ACQUIRE:LENGTH 0");
              }
             }); //addActionListener

            item = new JMenuItem("12500"); /* name this item and swap to 25000 if only 1 chan */
            submenu.add(item);
            item.addActionListener(new ActionListener()
             {
              public void actionPerformed(ActionEvent e)
              {
                memoryLength = 1;
                commandReply(":ACQUIRE:LENGTH 1");
              }
             }); //addActionListener

          } // StopMenu()
        } // class StopMenu
     } //class OffsetLabel
    } // class Waveform
    class ScopeDisplayMenu extends JPopupMenu
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      public ScopeDisplayMenu()
      {
        super();
        JMenuItem item;
        item = new JCheckBoxMenuItem("Circle samples");
        item.setMnemonic(KeyEvent.VK_C);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            JCheckBoxMenuItem theCheckBoxItem = (JCheckBoxMenuItem)e.getSource();
            bCircleSamples = theCheckBoxItem.getState();
            jpScope.repaint();
          }
         }); //addActionListener
        add(item);
        item = new JCheckBoxMenuItem("Show values");
        item.setMnemonic(KeyEvent.VK_V);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            JCheckBoxMenuItem theCheckBoxItem = (JCheckBoxMenuItem)e.getSource();
            bShowValues = theCheckBoxItem.getState();
            jpScope.repaint();
          }
         }); //addActionListener
        add(item);
        item = new JCheckBoxMenuItem("Show tooltips");
        item.setMnemonic(KeyEvent.VK_T);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            JCheckBoxMenuItem theCheckBoxItem = (JCheckBoxMenuItem)e.getSource();
            bSampleToolTips = theCheckBoxItem.getState();
            if (!bSampleToolTips)
              jpScope.setToolTipText(null);
          }
         }); //addActionListener
        add(item);
      } // ScopeDisplayMenu()
    } // class ScopeDisplayMenu
    class ScopeDisplayMouseAdapter extends MouseAdapter
    {
      private String sDefaultC2KThresh;
      private String sDefaultC2DThresh;
      private JMenu selectionMenu;
      private boolean bCouldBeSelecting;
      public ScopeDisplayMouseAdapter()
      {
        super();
        bCouldBeSelecting = false;
        selectionMenu = new JMenu("Selection");
        JMenuItem item = new JMenuItem("Dump to stdout");
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            dumpSelectionBoxValues(System.out);
          } //actionPerformed(ActionEvent)
         }); // adActionListener
        selectionMenu.add(item);
        item = new JMenuItem("Dump to file...");
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            JFileChooser chooser = new JFileChooser();
            chooser.showSaveDialog(jpScope);
            File f = chooser.getSelectedFile();
            if (f != null)
            {
              try
              {
                PrintStream ps = new PrintStream(f);
                dumpSelectionBoxValues(ps);
              }
              catch (Exception eOpenFile)
              { eOpenFile.printStackTrace(); }
            }
          } //actionPerformed(ActionEvent)
         }); // adActionListener
        selectionMenu.add(item);
        item = new JMenuItem("Read C2");
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            int clockLow, clockHigh;
            int dataLow, dataHigh;
            // Channel1 is data, 2 is clock
            int startIndex = (int)((selectBox.x - iInset) / xScale);
            int endIndex = (int)((selectBox.x + selectBox.width - iInset) / xScale);

            int clockLowCount = 0;
            float CLK, DATA;
            float CLKscale = channel2.fScale;
            float CLKoffset = channel2.fOffset;
            float DATAscale = channel1.fScale;
            float DATAoffset = channel1.fOffset;
            float fSampleDiv = channel1.fInterval;
            C2Reader c2X = new C2Reader();
            if (startIndex < 0)
              startIndex = 0;
            if (endIndex > channel1.aiPoints.length)
              endIndex = channel1.aiPoints.length;
            System.err.println("Selected to read " + (endIndex - startIndex) + " samples at " + fSampleDiv + "s sampling interval.");
            final int RESET_SAMPLE_MIN = (int)(2e-5f / fSampleDiv);
            if (RESET_SAMPLE_MIN < 40)
            {
              System.err.println("Too few samples. Reset is only " + RESET_SAMPLE_MIN + " long.");
              return;
            }
            final int C2D_VALID_AFTER_SAMPLES = (int)(C2Reader.TIMING_TDV / fSampleDiv) + 1;
            /*if (C2D_VALID_AFTER_SAMPLES < 2)
            {
              System.err.println("Too few samples. Cannot guarantee data read from slave.");
              return;
            }*/
            System.err.println("C2D valid on read after " + C2D_VALID_AFTER_SAMPLES + " samples");
            String s = JOptionPane.showInputDialog(jfThis, "Enter threshold voltage for CLK (units are Volts)", sDefaultC2KThresh);
            if (s == null)
              return;
            sDefaultC2KThresh = s;
            float clockThresh = Float.parseFloat(s);
            s = JOptionPane.showInputDialog(jfThis, "Enter threshold voltage for DATA (units are Volts)", sDefaultC2DThresh);
            if (s == null)
              return;
            sDefaultC2DThresh = s;
            float dataThresh = Float.parseFloat(s);
            for (int i = startIndex; i < endIndex; i++)
            {
              CLK = CLKscale * channel2.aiPoints[i] / 25f;
              if (c2X.isReadingSlave()) // C2D valid AFTER C2K
                DATA = DATAscale * channel1.aiPoints[i + C2D_VALID_AFTER_SAMPLES] / 25f;
              else
                DATA = DATAscale * channel1.aiPoints[i] / 25f;
              // System.err.println("CLK(" + channel2.aiPoints[i] + ")=" + CLK + ", DATA(" + channel1.aiPoints[i] + ")=" + DATA);
              if (CLK > clockThresh) // CLK hi
              {
                if (clockLowCount > 0) // rising edge
                {
                  if (clockLowCount > RESET_SAMPLE_MIN)
                    System.err.println("RESET");
                  else
                  {
                    c2X.strobe(DATA > dataThresh);
                    if (c2X.isStopped())
                      System.err.println(c2X);
                  }
                clockLowCount = 0;
                }
              } // rising edge on CLK
              else // C2K lo
                clockLowCount++;
            } // for all samples in selection
            System.err.println("C2 finished reading samples: " + c2X);
          } //actionPerformed
         }); // Read_C2.addActionListener
        selectionMenu.add(item);
      }
      public void mousePressed(MouseEvent e)
      {
        switch (e.getButton())
        {
          case MouseEvent.BUTTON1:
            jpScope.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
            selectBox.setStart(e.getX(), e.getY());
            bCouldBeSelecting = true;
            break;
          case MouseEvent.BUTTON2:
            break;
          case MouseEvent.BUTTON3:
            if (selectBox.contains(e.getPoint()))
              jpScopeMenu.add(selectionMenu);
            else
              jpScopeMenu.remove(selectionMenu);
            jpScopeMenu.show(jpScope, e.getX(), e.getY());
            break;

        } //switch getButton
      }
      public void mouseMoved(MouseEvent e)
      {
        // System.err.println(e);
        if (e.getButton() == MouseEvent.NOBUTTON)
          jpScope.setMousePos(e.getX(), e.getY());
      }
      public void mouseDragged(MouseEvent e)
      {
        // System.err.println(e);
        // System.err.println(selectBox);
        if (bCouldBeSelecting)
          selectBox.add(e.getX(), e.getY());
      }
      public void mouseReleased(MouseEvent e)
      {
        bCouldBeSelecting = false;
        jpScope.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
      }
      public void dumpSelectionBoxValues(PrintStream pStream)
      {
        int x = selectBox.x;
        int y = selectBox.y;
        int x2 = x + selectBox.width;
        int y2 = y + selectBox.height;
        int wavStartIndex = (int)((x - iInset) / xScale);
        int wavEndIndex = (int)((x2 - iInset) / xScale);
      // float yOffset = 25f * w.fOffset / w.fScale; // 25 = 200 / 8 divisions
      // float thisY = yZero - (w.aiPoints[x] + yOffset) * yScale;
        float[][] afChan = new float[2][];
        int indexMax = wavEndIndex - wavStartIndex;
        float fSample;
        int iSample;
        int wavTopValue;
        int wavLowValue;
        if (channel1.isAvailable)
        {
          wavTopValue = (int)((yZero - y) / yScale - 25f * channel1.fOffset / channel1.fScale);
          wavLowValue = (int)((yZero - y2) / yScale - 25f * channel1.fOffset / channel1.fScale);
          afChan[0] = new float[indexMax];
          for (int i = wavStartIndex; i < wavEndIndex; i++)
          {
            iSample = channel1.aiPoints[i];
            if (iSample < wavLowValue)
              iSample = wavLowValue;
            else if (iSample > wavTopValue)
              iSample = wavTopValue;
            afChan[0][i - wavStartIndex] = channel1.pointValue(iSample) + channel1.fOffset;
            // System.err.println("Sample " + i + " = " + channel1.pointValueString2(channel1.pointValue(iSample)));
          }
        } // if channel1.isAvailable
        if (channel2.isAvailable)
        {
          wavTopValue = (int)((yZero - y) / yScale - 25f * channel2.fOffset / channel2.fScale);
          wavLowValue = (int)((yZero - y2) / yScale - 25f * channel2.fOffset / channel2.fScale);
          afChan[1] = new float[indexMax];
          for (int i = wavStartIndex; i < wavEndIndex; i++)
          {
            iSample = channel2.aiPoints[i];
            if (iSample < wavLowValue)
              iSample = wavLowValue;
            else if (iSample > wavTopValue)
              iSample = wavTopValue;
            afChan[1][i - wavStartIndex] = channel2.pointValue(iSample) + channel2.fOffset;
            // System.err.println("Sample " + i + " = " + channel2.pointValueString2(channel2.pointValue(iSample)));
          }
        } // if channel2.isAvailable
        for (int i = 0; i < indexMax; i++)
          if (afChan[0] != null)
          {
            pStream.print(afChan[0][i]);
            if (afChan[1] != null)
              pStream.println(" " + afChan[1][i]);
            else
              pStream.println();
          }
          else
            if (afChan[1] != null)
              pStream.println(afChan[1][i]);
      } // dumpSelectionBoxValues
    } // class ScopeDisplayMouseAdapter
    class SelectRect extends Rectangle
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      public boolean isVisible;
      private int startX;
      private int startY;
      public SelectRect(int x, int y)
      {
        super(x, y);
        isVisible = false;
        startX = x;
        startY = y;
      }
      public void setStart(int x, int y)
      {
        this.setBounds(x, y, 1, 1);
        jpScope.repaint();
        isVisible = true;
        startX = x;
        startY = y;
      }
      public void add(int x, int y)
      {
        this.setBounds(Math.min(x, startX)
         , Math.min(y, startY)
         , Math.abs(x - startX)
         , Math.abs(y - startY));
        jpScope.repaint();
      }
      public void paintBox(Graphics g)
      {
        // System.err.println("paintBox");
        g.setXORMode(Color.LIGHT_GRAY);
        // g.fillRect(rect.x, rect.y, lastRect.width, lastRect.height);
        g.fillRect(this.x, this.y, this.width, this.height);
        g.setPaintMode();
      }
    } // class SelectRect
    class GDS2062Label extends JLabel implements MouseListener
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      public GDS2062Label()
      {
        super("GDS-2062");
        setForeground(Color.LIGHT_GRAY);
        setBackground(Color.WHITE);
        setOpaque(true);
        addMouseListener(this);
        setToolTipText("Click to fetch data from scope");
        JPopupMenu popup = new JPopupMenu();
        this.setComponentPopupMenu(popup);
        JMenuItem item = new JMenuItem("About GDS2062...");
        item.setMnemonic(KeyEvent.VK_A);
        popup.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            Package thisPackage = this.getClass().getPackage();
            JOptionPane.showMessageDialog(jpScope
             , thisPackage.getImplementationTitle()
             + "\nversion " + thisPackage.getImplementationVersion()
             + "\nFree software, see the file GPL.txt"
             , "About " + thisPackage.getImplementationTitle()
             , JOptionPane.PLAIN_MESSAGE);
          }
         }); // About...addActionListener
        item = new JMenuItem("Comms retries...");
        item.setMnemonic(KeyEvent.VK_C);
        popup.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            String s = JOptionPane.showInputDialog(jpScope
             , "Communications retries make fetching scope data more reliable.\nNumber of retries between 1 and 7 are good!"
             , iCommRetries);
            if (s != null)
              iCommRetries = Integer.parseInt(s);
          }
         });
      }
      public void mouseClicked(MouseEvent e)
      {
        if (e.getButton() == MouseEvent.BUTTON1)
          jpScope.refresh();
      }
      public void mouseEntered(MouseEvent e) {}
      public void mouseExited(MouseEvent e) {}
      public void mousePressed(MouseEvent e) {}
      public void mouseReleased(MouseEvent e) {}
    } // class GDS2062Label
    class TimeOffsetLabel extends JLabel
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      public TimeOffsetLabel()
      {
        super("TimeOffset");
        setForeground(CHANNEL_COLOUR[2]);
        JPopupMenu popup = new JPopupMenu();
        setComponentPopupMenu(popup);
        JMenuItem item;
        item = new JMenuItem("Zero");
        popup.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            setTimeOffset("0s");
          }
         }); // addActionListener
        item = new JMenuItem("Input...");
        popup.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            String s = JOptionPane.showInputDialog(jpScope, "Enter new time offset", sTimeOffset);
            if (s != null)
              setTimeOffset(s);
          }
         }); // addActionListener
      } // TimeOffsetLabel()
      private void setTimeOffset(String s)
      {
        commandReply(":TIMEBASE:DELAY " + time2Float(s));
        this.refresh();
      }
      private void refresh()
      {
        byte[] b = commandReply(":TIMEBASE:DELAY?");
        sTimeOffset = bytes2StringNoLF(b);
        fTimeOffset = time2Float(sTimeOffset);
				try
				{
					this.setText(prettyNumber(sTimeOffset) + "s");
				}
				catch(Exception e)
				{
					e.printStackTrace();
				}
      }
    } // class TimeOffsetLabel
      
    class TimeDivLabel extends JLabel
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      public TimeDivLabel()
      {
        super("TimeDiv");
        setForeground(Color.GREEN);
        JPopupMenu popup = new JPopupMenu();
        setComponentPopupMenu(popup);
        StringPair[] spTimeDivOptions = {
         new StringPair("1ns", "1e-9"),
         new StringPair("2.5ns", "2.5e-9"),
         new StringPair("5ns", "5e-9"),
         new StringPair("10ns", "10e-9"),
         new StringPair("25ns", "25e-9"),
         new StringPair("50ns", "50e-9"),
         new StringPair("100ns", "100e-9"),
         new StringPair("250ns", "250e-9"),
         new StringPair("500ns", "500e-9"),
         new StringPair("1us", "1e-6"),
         new StringPair("2.5us", "2.5e-6"),
         new StringPair("5us", "5e-6"),
         new StringPair("10us", "10e-6"),
         new StringPair("25us", "25e-6"),
         new StringPair("50us", "50e-6"),
         new StringPair("100us", "100e-6"),
         new StringPair("250us", "250e-6"),
         new StringPair("500us", "500e-6"),
         new StringPair("1ms", "1e-3"),
         new StringPair("2.5ms", "2.5e-3"),
         new StringPair("5ms", "5e-3"),
         new StringPair("10ms", "10e-3"),
         new StringPair("25ms", "25e-3"),
         new StringPair("50ms", "50e-3"),
         new StringPair("100ms", "100e-3"),
         new StringPair("250ms", "250e-3"),
         new StringPair("500ms", "500e-3"),
         new StringPair("1s", "1"),
         new StringPair("2.5s", "2.5"),
         new StringPair("5s", "5"),
         new StringPair("10s", "10")};
        JMenuItem item;
        for (StringPair sp : spTimeDivOptions)
        {
          item = new JMenuItem(sp.first);
          item.addActionListener(new TimeDivActionListener(sp));
          popup.add(item);
        } // for all timediv string pairs
      } // TimeDivLabel()
      class TimeDivActionListener implements ActionListener
      {
        private StringPair sp;
        public TimeDivActionListener(StringPair sp) { this.sp = sp; }
        public void actionPerformed(ActionEvent e) { setTimeDiv(sp.first, sp.second); }
      } // class TimeDivActionListener
      class StringPair
      {
        public String first, second;
        public StringPair(String s1, String s2){ first = s1; second = s2; }
      } // class StringPair
      public void setTimeDiv(String label, String command)
      {
        commandReply(":TIMEBASE:SCALE " + command);
        setText(label);
      }
    } // class TimeDivLabel
    class TriggerStatusLabel extends JLabel
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      private String sTriggerCouple;
      private String sTriggerLevel;
      private float fTriggerLevel;
      private String sMode = "?";
      private String sSlope = "?";
      private String sSource = "?";
      private String sType = "?";
      private final static int TEXT_BASELINE = 12;
      public TriggerStatusLabel()
      {
        super("T CH? ???? ?");
        JPopupMenu popup = new JPopupMenu();
        setComponentPopupMenu(popup);
        JMenu submenu;
        JMenuItem item;
        submenu = new JMenu("Channel");
        submenu.setMnemonic(KeyEvent.VK_C);
        popup.add(submenu);
        item = new JMenuItem("CH1");
        item.setMnemonic(KeyEvent.VK_1);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:SOURCE 0"); refresh(); }
         }); // CH1.addActionListener
        item = new JMenuItem("CH2");
        item.setMnemonic(KeyEvent.VK_2);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:SOURCE 1"); refresh(); }
         }); // CH2.addActionListener
        submenu = new JMenu("Type");
        submenu.setMnemonic(KeyEvent.VK_C);
        popup.add(submenu);
        item = new JMenuItem("Edge");
        item.setMnemonic(KeyEvent.VK_E);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:TYPE 0"); refresh(); }
         }); // EDGE.addActionListener
        item = new JMenuItem("Video");
        item.setMnemonic(KeyEvent.VK_V);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:TYPE 1"); refresh(); }
         }); // Video.addActionListener
         item = new JMenuItem("Pulse");
        item.setMnemonic(KeyEvent.VK_P);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:TYPE 2"); refresh(); }
         }); // Pulse.addActionListener
         item = new JMenuItem("Delay");
        item.setMnemonic(KeyEvent.VK_D);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:TYPE 3"); refresh(); }
         }); // Delay.addActionListener
        submenu = new JMenu("Slope");
        submenu.setMnemonic(KeyEvent.VK_S);
        popup.add(submenu);
        item = new JMenuItem("Rising");
        item.setMnemonic(KeyEvent.VK_R);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:SLOPE 0"); refresh(); }
         }); // Rising.addActionListener
        item = new JMenuItem("Falling");
        item.setMnemonic(KeyEvent.VK_F);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:SLOPE 1"); refresh(); }
         }); // Falling.addActionListener
        submenu = new JMenu("Mode");
        submenu.setMnemonic(KeyEvent.VK_M);
        popup.add(submenu);
        item = new JMenuItem("Auto Level");
        item.setMnemonic(KeyEvent.VK_L);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:MODE 0"); }
         }); // AutoLevel.addActionListener
        item = new JMenuItem("Auto");
        item.setMnemonic(KeyEvent.VK_A);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:MODE 1"); }
         }); // Auto.addActionListener
        item = new JMenuItem("Normal");
        item.setMnemonic(KeyEvent.VK_N);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:MODE 2"); }
         }); // Normal.addActionListener
        item = new JMenuItem("Single");
        item.setMnemonic(KeyEvent.VK_S);
        submenu.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          { commandReply(":TRIGGER:MODE 3"); }
         }); // Single.addActionListener
        item = new JMenuItem("Level...");
        item.setMnemonic(KeyEvent.VK_L);
        popup.add(item);
        item.addActionListener(new ActionListener()
         {
          public void actionPerformed(ActionEvent e)
          {
            try
            {
              String s = JOptionPane.showInputDialog(jpScope, "Enter trigger level", sTriggerLevel);
              if (s != null)
              {
                commandReply(":TRIGGER:LEVEL " + GDS2062.parseVolt(s));
                refresh();
              }
            }
            catch (Exception eTrigLev)
            {
              eTrigLev.printStackTrace();
            }
          }
         }); // Falling.addActionListener
         } // TriggerStatusLabel()
      public void paintComponent(Graphics g)
      {
        this.setSize(120, 15);
        g.setColor(Color.RED);
        g.fillRect(0, 0, 10, 15);
        g.setColor(Color.WHITE);
        g.drawString("T", 2, TEXT_BASELINE);
        if (sSource.equals("0"))
        {
          g.setColor(Color.YELLOW);
          g.drawString("CH1", 12, TEXT_BASELINE);
        }
        else if (sSource.equals("1"))
        {
          g.setColor(Color.CYAN);
          g.drawString("CH2", 12, TEXT_BASELINE);
        }
        else
        {
          g.setColor(Color.RED);
          g.drawString("???", 12, TEXT_BASELINE);
        }
        g.setColor(Color.WHITE);
        if (sType.equals("0"))
          g.drawString("EDGE", 50, TEXT_BASELINE);
        else if (sType.equals("1"))
          g.drawString("VIDEO", 50, TEXT_BASELINE);
        else if (sType.equals("2"))
          g.drawString("PULSE", 50, TEXT_BASELINE);
        else if (sType.equals("3"))
          g.drawString("DELAY", 50, TEXT_BASELINE);
        else
          g.drawString("Type?", 50, TEXT_BASELINE);
        if (sSlope.equals("0"))
        {
          g.drawLine(100, TEXT_BASELINE, 103, TEXT_BASELINE);
          g.drawLine(103, TEXT_BASELINE, 109, 3);
          g.drawLine(109, 3, 112, 3);
        }
        else if (sSlope.equals("1"))
        {
          g.drawLine(109, TEXT_BASELINE, 112, TEXT_BASELINE);
          g.drawLine(109, TEXT_BASELINE, 103, 3);
          g.drawLine(100, 3, 103, 3);
        }
        else
          g.drawString("?", 100, TEXT_BASELINE);
      }
      public void refresh()
      {
        byte[] b = null;
        b = commandReply(":TRIGGER:COUPLE?");
        sTriggerCouple = bytes2StringNoLF(b);
        b = commandReply(":TRIGGER:LEVEL?");
        setTriggerLevel(bytes2StringNoLF(b));
        b = commandReply(":TRIGGER:MODE?");
        sMode = bytes2StringNoLF(b);
        b = commandReply(":TRIGGER:SLOPE?");
        sSlope = bytes2StringNoLF(b);
        b = commandReply(":TRIGGER:SOURCE?");
        sSource = bytes2StringNoLF(b);
        b = commandReply(":TRIGGER:TYPE?");
        sType = bytes2StringNoLF(b);
        this.repaint();
      }
      private void setTriggerLevel(String s)
      {
        sTriggerLevel = s;
        fTriggerLevel = GDS2062.parseVolt(s);
      }
    } // class TriggerStatusLabel
    class StopLabel extends JLabel
    {
      /* for Serializable interface */
      private static final long serialVersionUID = 0;
      private boolean bStopped;
      public StopLabel()
      {
        super();
        setStopped(true);
        setOpaque(true);
        setComponentPopupMenu(new StopMenu());
      }
      private void setStopped(boolean isStopped)
      {
        bStopped = isStopped;
        if (bStopped)
        {
          setForeground(Color.WHITE);
          setBackground(Color.RED);
          setText("STOP");
        }
        else // Running?
        {
          setForeground(Color.BLACK);
          setBackground(Color.GREEN);
          setText("Trig?");
        }
      }
      private void stopScope()
      {
        setStopped(true);
        commandReply(":STOP");
      }
      private void runScope()
      {
        setStopped(false);
        commandReply(":RUN");
        int memoryLength = 0; // 500 samples
        if (channel1 != null)
          memoryLength = channel1.memoryLength;
        else
          if (channel2 != null)
            memoryLength = channel2.memoryLength;
        commandReply(":ACQUIRE:LENGTH " + memoryLength); // Scope seems to forget?
      }
      private class StopMenu extends JPopupMenu
      {
        /* for Serializable interface */
        private static final long serialVersionUID = 0;
        public StopMenu()
        {
          super();
          JMenuItem item;
          item = new JMenuItem("Stop");
          item.setMnemonic(KeyEvent.VK_S);
          item.addActionListener(new ActionListener()
           {
            public void actionPerformed(ActionEvent e)
            {
              stopScope();
            }
           }); //addActionListener
          add(item);
          item = new JMenuItem("Run");
          item.setMnemonic(KeyEvent.VK_R);
          item.addActionListener(new ActionListener()
           {
            public void actionPerformed(ActionEvent e)
            {
              runScope();
            }
           }); //addActionListener
          add(item);
        } // StopMenu()
      } // class StopMenu
    } // class StopLabel
  } // class ScopeDisplay
	public static float volt2Float(String s)
	{
		return pretty2Float(s.replace("V", ""));
	}
  public static float time2Float(String s)
  {
    return pretty2Float(s.replace("s", ""));
  }
	public static float pretty2Float(String s)
	{
    if (s.indexOf("m") > -1) // milli
      return Float.parseFloat(s.replace("m", "")) / 1e3f;
    else if (s.indexOf("u") > -1) // micro
      return Float.parseFloat(s.replace("u", "")) / 1e6f;
    else if (s.indexOf("n") > -1) // nano
      return Float.parseFloat(s.replace("n", "")) / 1e9f;
    else // seconds
      return Float.parseFloat(s);
	}
  public static String timeString(float f)
  {
    String s = "";
    if (f < 0)
    {
      s = "-";
      f = Math.abs(f);
    }
    if (f < 1e-6)
      return s + f / 1e-9 + "ns";
    else if (f < 1e-3)
      return s + f / 1e-6 + "us";
    else if (f < 1)
      return s + f / 1e-3 + "ms";
    else
      return s + format3places.format(f) + "s";
  }
  public static String byteString(byte[] b)
  {
    StringBuffer s = new StringBuffer(3 * b.length);
    for (byte y : b)
      s.append(Integer.toString(byte2UnsignedInt(y), 0x10) + ",");
    if (s.length() > 0)
      s.setLength(s.length() - 1);
    return s.toString();
  }
  public static int byte2UnsignedInt(byte b)
  {
    if (b < 0x00)
      return 0x100 + b;
    else
      return b;
  }
  public String bytes2StringNoLF(byte[] b)
  {
    if (b == null)
      return new String("");
    if (b.length < 1)
      return new String("");
    if (b[b.length - 1] == 0x0a)
      return new String(b, 0, b.length - 1);
    else
      return new String(b);
  }
  public String volt2String(float f)
  {
    if (Math.abs(f) < 1e-3)
      return Float.toString(f * 1e+6f) + "uV";
    else if (Math.abs(f) < 0)
      return Float.toString(f * 1e+3f) + "mV";
    else
      return Float.toString(f) + "V";
  }
  class C2Reader
  {
    private boolean bStop;
    private String status;
    private int bitCount;
    private int byteCount;
    private String sLast;
    private int byteData;
    // on reads, slave replies AFTER strobe.
    public final static float TIMING_TDV = 20e-9f; // 20ns
    public C2Reader()
    {
      bStop = true;
      status = "stopped";
      sLast = "";
    }
    public void strobe(boolean data)
    {
      // System.err.println("strobe " + data);
      if (status.equals("stopped"))
      {
        bStop = false;
        status = "started";
      } // status = stopped
      else if (status.equals("started"))
        if (data)
          status = "W";
        else
          status = "R";
      else if (status.equals("R") || status.equals("W"))
      {
        if (data)
          status += "A";
        else
          status += "D";
        sLast += status;
        bitCount = 0;
        byteData = 0;
      }
      else if (status.equals("WA") || status.equals("RA")) // address write or read
      { // get 8 bits of address 
        byteData |= (data?1<<bitCount:0);
        bitCount++;
        if (bitCount >= 8)
        {
          status = "waitstop";
          String sHex = Integer.toString(byteData, 0x10);
          if (sHex.length() < 2)
            sLast += "0" + sHex;
          else
            sLast += sHex;
        }
      }
      else if (status.equals("waitstop"))
      {
        status = "stopped";
        bStop = true;
      }
      else if (status.equals("WD")) // data write
      {
        byteCount = data?3:1;
        status = "WD2";
      }
      else if (status.equals("WD2")) // second bit of length
      {
        byteCount += data?1:0;
        status = "WDdata";
        byteData = 0;
        bitCount = 0;
      }
      else if (status.equals("WDdata"))
      {
        byteData |= (data?1<<bitCount:0);
        bitCount++;
        if (bitCount >= 8)
        {
          String sHex = Integer.toString(byteData, 0x10);
          if (sHex.length() < 2)
            sLast += "0" + sHex;
          else
            sLast += sHex;
          byteData = 0;
          bitCount = 0;
          byteCount--;
          if (byteCount < 1)
            status = "WDwait";
        }
      }
      else if (status.equals("WDwait"))
      {
        if (data)
          status = "waitstop";
      }
      else if (status.equals("RD")) // data read
      {
        byteCount = data?3:1;
        status = "RD2";
      }
      else if (status.equals("RD2")) // second bit of length
      {
        byteCount += data?1:0;
        status = "RDwait";
        byteData = 0;
        bitCount = 0;
      }
      else if (status.equals("RDwait"))
      {
        if (data)
          status = "RDdata";
      }
      else if (status.equals("RDdata"))
      {
        byteData |= (data?1<<bitCount:0);
        bitCount++;
        if (bitCount >= 8)
        {
          String sHex = Integer.toString(byteData, 0x10);
          if (sHex.length() < 2)
            sLast += "0" + sHex;
          else
            sLast += sHex;
          byteData = 0;
          bitCount = 0;
          byteCount--;
          if (byteCount < 1)
            status = "waitstop";
        }
      }
      else
        System.err.println("C2Reader bad status: " + status);
      // System.err.println(status);
    }
    public boolean isReadingSlave()
    {
        return status.equals("RA") || status.equals("RDwait") || status.equals("WDwait") || status.equals("RDdata");
    }
    public boolean isStopped() { return bStop; }
    public String toString()
    {
      if (bStop)
        return "C2Reader: " + sLast;
      else
        return "C2Reader not stopped: " + status;
    }
  }
  public static float parseVolt(String s)
  {
    if (s == null)
      return 0f;
    if (s.indexOf("V") > -1)
    {
      s = s.replace("V", "");
      if (s.indexOf("m") > -1) // millivolts
        return Float.parseFloat(s.replace("m", "")) / 1000.0f;
			else if (s.indexOf("u") > -1) // microvolts
				return Float.parseFloat(s.replace("u", "")) / 1e+6f;
      else // volts
        return Float.parseFloat(s);
    }
    return 0f;
  } // float parseVolt(String)
	public static String prettyNumber(String scope) throws Exception
	{
		String s = "";
		if (scope.charAt(0) == '-')
		{
			s += "-";
			scope = scope.substring(1);
		}
		String sNumber = scope.substring(0, 1) + scope.substring(2, 5);
		switch (scope.charAt(6))
		{
			case '-':
				switch (scope.charAt(7))
				{
					case '0':
						switch (scope.charAt(8))
						{
							case '1':
								s += sNumber.substring(0, 3) + "." + sNumber.substring(3) + "m";
								break;
							case '2':
								s += sNumber.substring(0, 2) + "." + sNumber.substring(2) + "m";
								break;
							case '3':
								s += sNumber.substring(0, 1) + "." + sNumber.substring(1) + "m";
								break;
							case '4':
								s += sNumber.substring(0, 3) + "." + sNumber.substring(3) + "u";
								break;
							case '5':
								s += sNumber.substring(0, 2) + "." + sNumber.substring(2) + "u";
								break;
							case '6':
								s += sNumber.substring(0, 1) + "." + sNumber.substring(1) + "u";
								break;
							case '7':
								s += sNumber.substring(0, 3) + "." + sNumber.substring(3) + "n";
								break;
							case '8':
								s += sNumber.substring(0, 2) + "." + sNumber.substring(2) + "n";
								break;
							case '9':
								s += sNumber.substring(0, 1) + "." + sNumber.substring(1) + "n";
								break;

							default:
								throw new Exception("Not expecting '" + scope.charAt(8) + "' in number from scope");
						} // switch -n.nnn-E0([0-9])
						break;
					case '1':
						switch (scope.charAt(8))
						{
							case '0': // E-10
								s += sNumber.substring(0, 3) + "." + sNumber.substring(3) + "p";
								break;
							case '1': // E-11
								s += sNumber.substring(0, 2) + "." + sNumber.substring(2) + "p";
								break;
							case '2': // E-12
								s += sNumber.substring(0, 1) + "." + sNumber.substring(1) + "p";
								break;
							case '3': // E-13 hundreds of femtoseconds!
								s += sNumber.substring(0, 3) + "." + sNumber.substring(3) + "f";
								break;
						}  // switch -n.nnn-E1([0-9])
						break;
					default:
						throw new Exception("Zetta-scale not supported!");
				} // switch -n.nnn-E([01])[0-9]
				break;
			case '+':
				switch (scope.charAt(8))
				{
					case '0':
						s += sNumber.substring(0, 1) + "." + sNumber.substring(1) + "";
						break;
					case '1':
						s += sNumber.substring(0, 2) + "." + sNumber.substring(2) + "";
						break;

					default:
						throw new Exception("Not expecting '" + scope.charAt(8) + "' in number from scope");
				}
				break;
			default:
				throw new Exception("Neither + nor - in scope string");
		}
		return s;
	}
} // class GDS2062
